14  Representação Vetorial: Embeddings e Positional Encoding

Em modelos de Processamento de Linguagem Natural (NLP) baseados em Deep Learning, especificamente na arquitetura Transformer, os dados brutos (texto) não podem ser processados diretamente. O primeiro passo crítico é a transformação de tokens discretos (índices inteiros de um vocabulário) em representações vetoriais contínuas e densas, enriquecidas com informações sobre a ordem da sequência.

Este capítulo detalha a arquitetura da camada de entrada, composta por dois subcomponentes principais: Input Embeddings e Positional Encoding.

14.1 1. Input Embeddings (Camada de Embeddings)

A camada de Embeddings atua como uma tabela de consulta (lookup table) aprendível. Enquanto a representação One-Hot Encoding gera vetores esparsos e de altíssima dimensionalidade (tamanho do vocabulário), os Embeddings projetam esses tokens em um espaço vetorial denso de dimensão inferior (\(d_{model}\)), onde a proximidade geométrica reflete a similaridade semântica.

14.1.1 Características Técnicas

  • Mapeamento: Cada ID de token \(x\) é mapeado para um vetor \(v \in \mathbb{R}^{d_{model}}\).
  • Dimensionalidade (\(d_{model}\)): Um hiperparâmetro da arquitetura (ex: 512 no Transformer original, 4096 no GPT-3).
  • Escalonamento de Variância: No artigo original “Attention Is All You Need”, os pesos dos embeddings são multiplicados por \(\sqrt{d_{model}}\). Isso é feito para contrabalancear a magnitude do produto escalar na camada de atenção subsequente, auxiliando na estabilidade dos gradientes durante o treinamento.

14.2 2. Positional Encoding (Codificação Posicional)

Diferente de Redes Neurais Recorrentes (RNNs) ou LSTMs, a arquitetura Transformer não processa dados sequencialmente; ela processa a sequência inteira em paralelo. Consequentemente, o mecanismo de Self-Attention é invariante à permutação. Sem uma injeção explícita de posição, o modelo veria as frases “O cão mordeu o homem” e “O homem mordeu o cão” como idênticas em termos de composição de tokens.

Para resolver isso, injetamos um vetor de Positional Encoding (PE) somando-o ao vetor de Embedding.

14.2.1 Implementação Matemática

A codificação posicional utiliza frequências de ondas senoidais e cossenos de diferentes comprimentos de onda. Para uma posição \(pos\) na sequência e uma dimensão \(i\) dentro do vetor de embedding:

\[ PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) \]

\[ PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) \]

Por que esta fórmula? 1. Valores Determinísticos: Não requer parâmetros aprendíveis extras (embora embeddings posicionais aprendíveis também sejam usados em modelos modernos como BERT). 2. Relatividade Linear: Para qualquer deslocamento fixo \(k\), \(PE_{pos+k}\) pode ser representado como uma função linear de \(PE_{pos}\). Isso facilita para o modelo aprender a atender posições relativas (ex: o token anterior ou o próximo). 3. Extrapolação: Permite que o modelo processe sequências mais longas do que as vistas durante o treinamento.

14.3 3. Arquitetura do Fluxo de Dados

A combinação dessas duas camadas resulta na entrada final para os blocos do Transformer. A operação é uma soma elemento a elemento (element-wise addition), seguida geralmente por uma camada de Dropout para regularização.

14.3.1 Diagrama de Fluxo

graph TD
    subgraph "Pré-processamento"
    RawText[Texto Bruto] --> Tokenizer[Tokenizador]
    Tokenizer --> TokenIDs[IDs dos Tokens (Inteiros)]
    end

    subgraph "Camada de Representação Vetorial"
    TokenIDs --> EmbedLayer[Embedding Lookup Table]
    EmbedLayer --> Scale[Escalar por sqrt(d_model)]
    
    PosIndex[Índices de Posição 0..N] --> PosEncCalc[Cálculo Seno/Cosseno]
    PosEncCalc --> PosVector[Vetor Positional Encoding]
    
    Scale --> Sum((Soma Element-wise))
    PosVector --> Sum
    
    Sum --> Dropout[Dropout Layer]
    end

    Dropout --> TransformerBlock[Bloco Transformer]

    style Sum fill:#f9f,stroke:#333,stroke-width:2px
    style EmbedLayer fill:#bbf,stroke:#333,stroke-width:2px
    style PosEncCalc fill:#bbf,stroke:#333,stroke-width:2px

14.4 4. Implementação de Referência (PyTorch)

Abaixo apresentamos uma implementação robusta e anotada, seguindo as especificações padrão da indústria.

import torch
import torch.nn as nn
import math

class InputEmbeddings(nn.Module):
    def __init__(self, d_model: int, vocab_size: int):
        """
        Args:
            d_model (int): Dimensão do vetor de embedding.
            vocab_size (int): Tamanho do vocabulário.
        """
        super().__init__()
        self.d_model = d_model
        self.vocab_size = vocab_size
        # Camada de Embedding padrão do PyTorch
        self.embedding = nn.Embedding(vocab_size, d_model)

    def forward(self, x):
        # Escalonamento por sqrt(d_model) conforme paper original
        return self.embedding(x) * math.sqrt(self.d_model)

class PositionalEncoding(nn.Module):
    def __init__(self, d_model: int, seq_len: int, dropout: float):
        """
        Args:
            d_model (int): Dimensão do modelo.
            seq_len (int): Comprimento máximo da sequência.
            dropout (float): Taxa de dropout.
        """
        super().__init__()
        self.dropout = nn.Dropout(dropout)

        # Cria uma matriz de (seq_len, d_model) com zeros
        pe = torch.zeros(seq_len, d_model)
        
        # Cria um vetor de posições (0, 1, ... seq_len-1)
        # Shape: (seq_len, 1)
        position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
        
        # Termo divisor para as frequências (10000^(2i/d_model))
        # Implementado em log-space para estabilidade numérica
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        
        # Aplica Seno aos índices pares (2i)
        pe[:, 0::2] = torch.sin(position * div_term)
        
        # Aplica Cosseno aos índices ímpares (2i+1)
        pe[:, 1::2] = torch.cos(position * div_term)

        # Adiciona dimensão de batch para facilitar o broadcast na soma: (1, seq_len, d_model)
        pe = pe.unsqueeze(0)

        # Registra como buffer (não é um parâmetro aprendível, mas faz parte do estado do modelo)
        self.register_buffer('pe', pe)

    def forward(self, x):
        """
        Args:
            x: Embeddings de entrada. Shape: (Batch_Size, Seq_Len, d_model)
        """
        # Soma o embedding com o positional encoding (até o comprimento da sequência atual)
        # x.requires_grad_(False) não é necessário pois pe é buffer, mas a soma mantém o gradiente de x
        x = x + self.pe[:, :x.shape[1], :]
        return self.dropout(x)

# Exemplo de uso integrado
class TransformerInputLayer(nn.Module):
    def __init__(self, vocab_size, d_model, max_len, dropout=0.1):
        super().__init__()
        self.embeddings = InputEmbeddings(d_model, vocab_size)
        self.positional_encoding = PositionalEncoding(d_model, max_len, dropout)
        
    def forward(self, x):
        x = self.embeddings(x)
        x = self.positional_encoding(x)
        return x

14.4.1 Análise do Código

  1. Estabilidade Numérica: No cálculo do div_term, utilizamos torch.exp e math.log. Matematicamente, \(e^{\ln(x)} = x\). Isso evita potências diretas de números muito grandes ou muito pequenos, mantendo a precisão em ponto flutuante.
  2. Buffers: O uso de register_buffer garante que a matriz de Positional Encoding seja salva junto com o modelo (state_dict), mas não seja atualizada pelo otimizador (Backpropagation), pois é fixa.
  3. Broadcasting: A forma do tensor pe é (1, seq_len, d_model). Isso permite que ele seja somado automaticamente a um batch de entradas (Batch, seq_len, d_model) sem necessidade de duplicação de memória.